package com.sikaryofficial.backend.service.activity.lottery;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.sikaryofficial.backend.config.ActivityConfig;
import com.sikaryofficial.backend.constant.PrizeTypeEnum;
import com.sikaryofficial.backend.domain.dto.req.prize.LotteryReq;
import com.sikaryofficial.backend.domain.entity.Activity;
import com.sikaryofficial.backend.domain.vo.PointConsumerVO;
import com.sikaryofficial.backend.manager.ActivityAttendManager;
import com.sikaryofficial.backend.message.body.LotteryMessage;
import com.sikaryofficial.backend.message.product.ProductProcess;
import com.sikaryofficial.backend.resolver.PropertiesTools;
import com.sikaryofficial.backend.service.activity.ActivityCacheService;
import com.sikaryofficial.backend.service.user.UserCacheService;
import com.sikaryofficial.common.core.domain.R;
import com.sikaryofficial.common.core.exception.ServiceException;
import com.sikaryofficial.common.security.utils.SecurityUtils;
import com.sikaryofficial.system.api.RemoteQrCodeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

import static com.sikaryofficial.backend.constant.ActivityConstant.EFFECT_NO;
import static com.sikaryofficial.backend.constant.i18n.ExceptionConstant.*;

/**
 * @author : qinjinyuan
 * @desc : 抽奖实现服务
 * @date : 2023/11/03 17:11
 */
@Component
@Slf4j
public class LotteryEngine {
    private static final long NO_PRIZE_ID = 0L;
    @Autowired
    private ActivityCacheService activityCacheService;
    @Resource
    private PropertiesTools propertiesTools;
    @Autowired
    private ProductProcess productProcess;
    @Resource
    private ActivityConfig activityConfig;
    @Autowired
    private UserCacheService userCacheService;
    @Autowired
    private RemoteQrCodeService remoteQrCodeService;
    @Autowired
    private ActivityAttendManager activityAttendManager;

    /**
     * 100%中奖，直到库存耗尽
     *
     * @param awards
     * @return
     */
    public synchronized PointConsumerVO drawForHundredPercent(List<PointConsumerVO> awards) {
        BigDecimal totalWinRatio = awards.stream().map(PointConsumerVO::getWinRatio).reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalWinRatio.compareTo(BigDecimal.ZERO) == 0) {
            log.error("所有奖品的中奖概率为0，无法进行抽奖");
            return null;
        }
        double rand = ThreadLocalRandom.current().nextDouble(0, 1) * totalWinRatio.doubleValue();
        double cumulativeRatio = 0;
        // 循环遍历奖项列表
        for (PointConsumerVO award : awards) {
            cumulativeRatio += award.getWinRatio().doubleValue();
            // 如果随机数小于等于累加的概率值，则返回该奖项
            if (rand <= cumulativeRatio && (award.getStock() == -1 || award.getStock() > 0)) {
                if (award.getStock() != -1) {
                    award.setStock(award.getStock() - 1);
                }
                return award;
            }
        }
        // 如果没有找到奖项（理论上不应该发生，因为总概率为100%），则返回第一个符合条件的奖项
        for (PointConsumerVO award : awards) {
            if (award.getStock() == -1 || award.getStock() > 0) {
                if (award.getStock() != -1) {
                    award.setStock(award.getStock() - 1);
                }
                return award;
            }
        }
        return null;
    }
    /**
     * 抽奖核心实现逻辑
     *
     * @param awards
     * @return
     */
    public synchronized PointConsumerVO draw(List<PointConsumerVO> awards) {
        BigDecimal totalWinRatio = awards.stream().map(PointConsumerVO::getWinRatio).reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalWinRatio.compareTo(BigDecimal.ZERO) == 0) {
            log.error("所有奖品的中奖概率为0，无法进行抽奖");
            return null;
        }
        // 概率由大到小排序
        awards = awards.stream().sorted((o1, o2) -> o2.getWinRatio().compareTo(o1.getWinRatio()))
                .collect(Collectors.toList());
        double rand = ThreadLocalRandom.current().nextDouble(0, 1) * totalWinRatio.doubleValue();
        double cumulativeRatio = 0;
        for (PointConsumerVO award : awards) {
            cumulativeRatio += award.getWinRatio().doubleValue();
            if (rand <= cumulativeRatio) {
                int stock = award.getStock();
                if (stock == -1 || stock > 0) {
                    if (stock != -1) {
                        stock--;
                        award.setStock(stock);
                    }
                    return award;
                }
            }
        }
        return null;
    }

    /**
     * 抽奖方法
     *
     * @param req
     * @return
     */
    public Long doLottery(LotteryReq req) {
        validateParameters(req.getActivityId(), SecurityUtils.getUserId());
        Long activityId = req.getActivityId();
        Long userId = SecurityUtils.getUserId();
        // 分支判定： 1. 抽奖次数是否已用完 2. 抽奖次数是否已用完 3. 奖品库存是否已用完
        Activity activityBaseInfo = activityCacheService.getActivityBaseInfo(activityId);
        // 活动有效性校验
        validActivity(activityBaseInfo);
        // 产品二维码判断
        List<PointConsumerVO> relationDataCache = getRelationDataCache(activityId, userId);
        // 奖项集合校验
        validRelationData(relationDataCache);
        String param = req.getParam();
        PointConsumerVO award = lotteryProcess(activityId, activityBaseInfo, relationDataCache, param);
        // 只要走完抽奖流程，就更新参与次数
        updateAttendNum(activityId, SecurityUtils.getUserId(), param);
        log.info("抽奖结果：" + award);
        // 更新缓存和发送MQ消息都在事务中完成，以保证数据一致性
        updateCacheAndSendMessage(activityId, award, param, relationDataCache);
        // 结果处理
        return processResult(relationDataCache, award);
    }

    private Long processResult(List<PointConsumerVO> relationDataCache, PointConsumerVO award) {
        // 潜在问题1：对relationDataCache进行空检查，避免空指针异常
        if (CollUtil.isEmpty(relationDataCache)) {
            return NO_PRIZE_ID;
        }
        if (Objects.isNull(award)) {
            // 使用优化后的Optional链式调用，简化代码
            return relationDataCache.stream()
                    .filter(item -> Objects.equals(item.getPrizeType(), PrizeTypeEnum.THANK_YOU.getCode()))
                    .findFirst()
                    .orElseGet(() -> {
                        PointConsumerVO newPrizeItem = new PointConsumerVO();
                        newPrizeItem.setPrizeId(NO_PRIZE_ID);
                        return newPrizeItem;
                    })
                    .getPrizeId();
        } else {
            // 实物中奖，再增加缓存数据
            if (Objects.equals(award.getPrizeType(), PrizeTypeEnum.PHYSICAL.getCode())) {
                activityCacheService.updateActivityPhysicalResult(award.getActivityId(), SecurityUtils.getUserId(), award.getPrizeId());
            }
            return award.getPrizeId();
        }
    }

    /**
     * 抽奖
     *
     * @param activityId
     * @param activityBaseInfo
     * @param relationDataCache
     * @param param
     * @return
     */
    private PointConsumerVO lotteryProcess(Long activityId, Activity activityBaseInfo, List<PointConsumerVO> relationDataCache, String param) {
        PointConsumerVO award;
        if (Objects.equals(activityConfig.getSpecialActivityId(), activityId)) {
            // 特殊隐藏活动，需要进行特殊处理: 移除指定二维码的 奖项，
            award = specialProcess(activityId, relationDataCache, param);
        } else {
            award = normalProcess(activityId, SecurityUtils.getUserId(), activityBaseInfo, relationDataCache);
        }
        return award;
    }

    private void updateCacheAndSendMessage(Long activityId, PointConsumerVO award, String param, List<PointConsumerVO> relationDataCache) {
        // 更新缓存
        activityCacheService.updateActivityRelationCache(relationDataCache);
        // 发送MQ消息，进行后续数据处理
        sendMQMessage(activityId, award, param);
        // 更新用户的月度实体中奖次数
        if (award != null && Objects.equals(award.getPrizeType(), PrizeTypeEnum.PHYSICAL.getCode())) {
            activityCacheService.addMonthlyPhysicalWinNum(SecurityUtils.getUserId());
        }
    }
    private void validRelationData(List<PointConsumerVO> relationDataCache) {
        if (CollUtil.isEmpty(relationDataCache)) {
            log.error("参数错误");
            throw new ServiceException(propertiesTools.getProperties(PARAM_ERROR));
        }
    }

    private void validActivity(Activity activityBaseInfo) {
        // 增加时间有效性校验  now() < endTime && now() > startTime
        if (Objects.isNull(activityBaseInfo) ||
                Objects.isNull(activityBaseInfo.getHasEffect()) ||
                EFFECT_NO.equals(activityBaseInfo.getHasEffect())
        ) {
            log.error("活动基本不存在 或者 已经下架");
            throw new ServiceException(propertiesTools.getProperties(ACTIVITY_TIME_ERROR));
        }
        if ((Objects.isNull(activityBaseInfo.getEndTime()) ||
                Objects.isNull(activityBaseInfo.getStartTime()) ||
                activityBaseInfo.getEndTime().before(new Date()) ||
                activityBaseInfo.getStartTime().after(new Date()))
        ) {
            log.error("活动未开始或已过期");
            throw new ServiceException(propertiesTools.getProperties(ACTIVITY_TIME_ERROR));
        }
        // 积分校验
        userCacheService.verifyUserPoints(SecurityUtils.getUserId(), activityBaseInfo.getPoints());
    }

    private void validateParameters(Long activityId, Long userId) {
        if (Objects.isNull(activityId) || activityId <= 0L || Objects.isNull(userId) || userId <= 0L) {
            log.error("参数错误");
            throw new ServiceException(propertiesTools.getProperties(PARAM_ERROR));
        }
    }

    private void updateAttendNum(Long activityId, Long userId, String param) {
        // 是否为特殊活动，并且带二维码参数
        if (Boolean.TRUE.equals(isSpecialActivity(activityId))) {
            activityCacheService.increaseActivityAttendNum(activityId, param);
        } else {
            activityCacheService.increaseActivityAttendDailyNum(activityId, userId);
        }
        // 异步：参与记录
        activityAttendManager.insertAttendRec(activityId, userId, param);
    }

    /**
     * 是否为特殊活动
     *
     * @param activityId
     * @return
     */
    private boolean isSpecialActivity(Long activityId) {
        return Objects.equals(activityConfig.getSpecialActivityId(), activityId);
    }

    private PointConsumerVO normalProcess(Long activityId, Long userId, Activity activityBaseInfo, List<PointConsumerVO> relationDataCache) {
        // 参与次数校验
        validAttendNum(activityId, activityCacheService.getActivityAttendDailyNum(activityId, userId),ACTIVITY_ATTEND_NUM_CONTROL_MSG);
        // 参与积分校验
        Integer points = activityBaseInfo.getPoints();
        if (Objects.isNull(points)) {
            log.error("活动参与积分配置错误 | 未配置");
            throw new ServiceException(propertiesTools.getProperties(PARAM_ERROR));
        }
        userCacheService.verifyUserPoints(userId, points);
        // 检查用户是否已经中过实物奖项
        boolean hasPhysicalPrize = relationDataCache.stream()
                .anyMatch(item -> Objects.nonNull(item.getPrizeType()) &&
                        PrizeTypeEnum.PHYSICAL.getCode().equals(item.getPrizeType()) &&
                        Boolean.TRUE.equals(activityCacheService.hasActivityPhysicalResult(activityId, userId, item.getPrizeId())));
        // 如果用户已经中过实物奖项，移除所有实物奖项
        if (hasPhysicalPrize) {
            relationDataCache.removeIf(item ->
                    Objects.nonNull(item.getPrizeType()) &&
                            PrizeTypeEnum.PHYSICAL.getCode().equals(item.getPrizeType()));
        }
        // 检查用户当月实物中奖次数
        if (Boolean.TRUE.equals(activityCacheService.hasMonthlyPhysicalWinLimit(userId))) {
            relationDataCache.removeIf(item ->
                    Objects.nonNull(item.getPrizeType()) &&
                            PrizeTypeEnum.PHYSICAL.getCode().equals(item.getPrizeType()));
        }
        return this.draw(relationDataCache);
    }

    private void sendMQMessage(Long activityId, PointConsumerVO award, String param) {
        // 奖项库存的实际扣减 和 积分扣减： 采用MQ处理方式
        productProcess.sendLotteryMessage(LotteryMessage.builder()
                .activityId(activityId)
                .param(param)
                .activityPrizeId(Objects.isNull(award) ? null : award.getActivityPrizeId())
                .userId(SecurityUtils.getUserId())
                .userName(SecurityUtils.getUsername())
                .prizeId(Objects.isNull(award) ? null : award.getPrizeId())
                .brand(SecurityUtils.getBrand())
                .build()
        );
    }

    /**
     * 特殊奖项处理
     *
     * @param activityId
     * @param relationDataCache
     * @param param
     * @return
     */
    private PointConsumerVO specialProcess(Long activityId,
                                           List<PointConsumerVO> relationDataCache, String param) {
        PointConsumerVO award;
        if (!Objects.equals(activityConfig.getSpecialActivityId(), activityId)) {
            log.error("活动不匹配");
            return null;
        }
        // 分享的活动没有此参数
        if (CharSequenceUtil.isBlank(param)) {
            log.error("特殊线下活动，但是二维码param参数为空");
            return null;
        }
        // 逻辑：增加二维码param是否存在
        R<Boolean> existProductQrCode = remoteQrCodeService.existProductQrCode(param);
        if (Objects.isNull(existProductQrCode) || Boolean.FALSE.equals(existProductQrCode.getData())) {
            log.error("二维码param参数不存在");
            return null;
        }
        // 参与次数校验-一个活动二维码只能参与一次； 和人没有关联
        validAttendNum(activityId, activityCacheService.getActivityAttendNum(activityId, param), ACTIVITY_ATTEND_QRCODE_NUM_CONTROL_MSG);
        // 隐藏活动处理 (匹配特殊二维码，)
        log.error("getSpecialQrCodes==================={}", activityConfig.getSpecialQrCodes());
        if (activityConfig.getSpecialQrCodes().contains(param)) {
            award = relationDataCache.stream().filter(item -> Objects.equals(item.getPrizeId(), activityConfig.getSpecialPrizeId()))
                    .findFirst().orElse(null);
        } else {
            award = this.drawForHundredPercent(removeSpecialAwards(relationDataCache));
        }
        return award;
    }

    private List<PointConsumerVO> removeSpecialAwards(List<PointConsumerVO> relationDataCache) {
        return relationDataCache.stream()
                .filter(item -> !Objects.equals(item.getPrizeId(), activityConfig.getSpecialPrizeId()))
                .collect(Collectors.toList());
    }

    /**
     * 参与次数校验
     *
     * @param activityId   活动id
     * @param curAttendNum 当前参与次数
     * @param errorMsg 异常信息
     */
    private void validAttendNum(Long activityId, Integer curAttendNum, String errorMsg) {
        Activity activityBaseInfo = activityCacheService.getActivityBaseInfo(activityId);
        Integer attendNum = activityBaseInfo.getAttendNumber();

        if (Objects.isNull(attendNum)) {
            log.error("活动基本信息参与次数设置有误");
            throw new ServiceException(propertiesTools.getProperties(PARAM_ERROR));
        }
        if (Objects.nonNull(curAttendNum) && curAttendNum + 1 > attendNum) {
            log.error("今日机会用完，明日继续,curAttendNum:{},attendNum:{}", curAttendNum, attendNum);
            throw new ServiceException(propertiesTools.getProperties(errorMsg));
        }
    }

    private List<PointConsumerVO> getRelationDataCache(Long activityId, Long userId) {
        validParam(activityId, userId);
        List<PointConsumerVO> relationDataCache = activityCacheService.getActivityRelationCache(activityId);
        if (CollUtil.isEmpty(relationDataCache)) {
            throw new ServiceException(propertiesTools.getProperties(PARAM_ERROR));
        }
        return relationDataCache;
    }

    /**
     * 参数校验
     *
     * @param activityId
     * @param userId
     */
    private void validParam(Long activityId, Long userId) {
        if (Objects.isNull(userId) || userId <= 0L) {
            throw new ServiceException(propertiesTools.getProperties(PARAM_ERROR));
        }
        if (Objects.isNull(activityId) || activityId <= 0L) {
            throw new ServiceException(propertiesTools.getProperties(PARAM_ERROR));
        }
    }
}
